-module(ffi).
-compile([export_all]).

-define(driver_name, "driver_libffi").
-define(UINT32_MAX, (4294967296-1)).
-define(UINT64_MAX, (18446744073709551616-1)).
-include("constants.hrl").
-include("ffi.hrl").

load_path(File) ->
    load_path(File, code:get_path()).

load_path(File, [D|Dirs]) ->
    Fname = D ++ "/" ++ File,
    case file:read_file_info(Fname) of
        {ok, _} -> {ok, D};
        _ -> load_path(File, Dirs)
    end;

load_path(_File, []) ->
    %%io:format("Error: ~s not found in code path\n", [File]),
    {error, enoent}.

is_windows() ->
    case os:type() of
        win32 -> true;
        {win32, _} -> true;
        _ -> false
    end.

so_or_dll() ->
    case is_windows() of
        true -> "dll";
        false -> "so"
    end.

load_driver(Name) ->
    {ok, Path} = load_path(Name ++ "." ++ so_or_dll()),
    erl_ddll:load_driver(Path, Name).

open_port() -> ffi:open_port(?driver_name).

open_port(Name) ->
    case load_driver(Name) of
        ok ->
            Port = open_port({spawn, Name}, []),
            {version, ?VERSION} = erlang:port_call(Port, ?CMD_GET_VERSION, []),
            Port;
	{error, ErrorDesc} ->
	    throw({driver_load, erl_ddll:format_error(ErrorDesc)})
    end.

get_port() -> get_port(?driver_name).

get_port(Name) ->
    case whereis(libffi_port) of
        undefined ->
            Port = open_port(Name),
            %% XXX Race condition?
            %% catch badarg and try again?
            register(libffi_port, Port),
            Port;
        Port ->
            Port
    end.

%% append .so if there's no extension already
append_so_ext(Filename) when is_list(Filename) ->
    case is_windows() of
        false ->
          case filename:extension(Filename) of 
	      []   -> Filename ++ ".so";
	      "so" -> Filename;
	      _    -> Filename %% but it's not clear whether we need ".so" or not
	  end;
	true -> Filename
    end.

%% stringlike() = string() | binary() | atom()
%% load_library([Port,] LibName[, CallConv]) -> {ok, Lib} | {error, Reason}
load_library(Port, LibName) when is_port(Port), is_list(LibName) ->
    Filename = append_so_ext(LibName),
    Res = erlang:port_call(Port, ?CMD_LOAD_LIBRARY, Filename),
    load_library_result(Filename, Res).

load_library(Port, LibName, CallConv)
  when is_port(Port), is_list(LibName), is_integer(CallConv) ->
    Filename = append_so_ext(LibName),
    Res = erlang:port_call(Port, ?CMD_LOAD_LIBRARY,
                           {Filename, CallConv}),
    load_library_result(Filename, Res).

load_library(LibName) ->
    load_library(get_port(), LibName).

%% The purpose of this hack is to make tests/error.erl look clean. If the
%% error message proves to be different across *nixes, then this function
%% should be removed
load_library_result(Name, Result = {error, {dlerror, ErrorDesc}}) ->
    NoSuchFile = Name ++ ": cannot open shared object file: No such file or directory",
    case ErrorDesc of
        NoSuchFile ->
	    {error, mod_not_found};
	_ -> Result
    end;
    
load_library_result(_Name, X) -> X.

%% get_proc([Port,] LibName, Function[, Params])
%% get_proc(Port, Lib, Function, Args?, ResType?, Params) when is_port(Port) ->
%%     erlang:port_call(Port, ?CMD_GET_PROC, {Lib, Function, Params}).

%% get_proc(Port, L, F) when is_port(Port) ->
%%     get_proc(Port, L, F, []);

%% get_proc(L, F, P) ->
%%     get_proc(get_port(), L, F, P).

%% get_proc(L, F) ->
%%     get_proc(get_port(), L, F).

call(Port, Lib, Function, Args, Params)
  when is_port(Port), is_list(Params) ->
    Res = erlang:port_call(Port, ?CMD_CALL_ASYNC,
                           {Lib, Function, Params, fix_args(Args)}),
    case Res of
        {async, AsyncId} ->
            receive
                {async_ok, AsyncId, Result} ->
                    fix_async_res(get_res_type(Params), Result);
                {async_error, AsyncId, Error} ->
                    throw(Error)
                %%Huh -> io:format("Huh?: ~p", [Huh])
            end;
        {error, Error} -> throw(Error)
    %% add after Timeout -> timeout
    end;

call(P, L, F, A, Param) when is_port(P) ->
    call(P, L, F, A, [Param]).

call(L, F, A, P) ->
    call(get_port(), L, F, A, P).

call(L, F, A) ->
    call(get_port(), L, F, A, []).

%% call/2 (without lib) should look for lib in Params from {proc, Fptr, Params}?

call_block(Port, Lib, Function, Args, Params)
  when is_port(Port), is_list(Params) ->
    Res = erlang:port_call(Port, ?CMD_CALL_BLOCK,
                           {Lib, Function, Params, fix_args(Args)}),
    case Res of
        {ok, Result} -> Result;
        {error, Error} -> throw(Error)
    end;

call_block(P, L, F, A, Param) when is_port(P) ->
    call_block(P, L, F, A, [Param]).

call_block(L, F, A, P) ->
    call_block(get_port(), L, F, A, P).

call_block(L, F, A) ->
    call_block(get_port(), L, F, A, []).

typeof(Port, Type) ->
    erlang:port_call(Port, ?CMD_TYPE_OF, Type).

typeof(Type) -> typeof(get_port(), Type).

sizeof(Port, Type) ->
    erlang:port_call(Port, ?CMD_SIZE_OF, Type).

sizeof(Type) -> sizeof(get_port(), Type).

get_res_type([]) -> int;
get_res_type([{res_type, Type}|_]) -> Type;
get_res_type([_|Rest]) -> get_res_type(Rest).

%% Args pre-processing:
%% {double/float/longdouble, integer()} -> {.., float()}
%% (it is done in Erlang, because it is easier than in C and because
%% double fit in 8 bytes, while big integer may take more space)

fix_arg({double, Int}) when is_integer(Int) -> {double, float(Int)};
fix_arg({float, Int}) when is_integer(Int) -> {float, float(Int)};
fix_arg({longdouble, Int}) when is_integer(Int) -> {longdouble, float(Int)};
fix_arg(Arg) -> Arg.
fix_args(Args) -> lists:map(fun fix_arg/1, Args).

%% Result post-processing:
%% 1. There's ERL_DRV_INT, but no ERL_DRV_UINT, so numbers
%%    in range [int_max+1, uint_max] will be mangled as negatives
fix_async_res(uint32, N) when N < 0 -> uint32_from_int32(N);
fix_async_res(uint, N)   when N < 0 -> uint32_from_int32(N);
fix_async_res(ulong, N)  when N < 0 -> uint32_from_int32(N);

%% 2. There's no ERL_DRV_INT64 either. It is coded as a tuple of 2 ints
fix_async_res(uint64, {A, B}) ->
    (uint32_from_int32(A) bsl 32) + uint32_from_int32(B);

fix_async_res(int64, {A, B}) ->
    int64_from_uint64(fix_async_res(uint64, {A, B}));

fix_async_res(ulonglong, R) -> fix_async_res(uint64, R);
fix_async_res(longlong, R) -> fix_async_res(int64, R);

fix_async_res(_Type, Value) -> Value.

uint32_from_int32(N) when N < 0 -> ?UINT32_MAX + 1 + N;
uint32_from_int32(N) -> N.

int64_from_uint64(N) when N > ?UINT64_MAX div 2 -> N - ?UINT64_MAX - 1;
int64_from_uint64(N) -> N.
